/**
 * Grouped Categories v1.0.13 (2016-01-13)
 *
 * (c) 2012-2016 Black Label
 *
 * License: Creative Commons Attribution (CC)
 */
(function (HC) {
    /*jshint expr:true, boss:true */
    var UNDEFINED = void 0,
        mathRound = Math.round,
        mathMin = Math.min,
        mathMax = Math.max,
        merge = HC.merge,
        pick = HC.pick,
    // #74, since Highcharts 4.1.10 HighchartsAdapter is only provided by the Highcharts Standalone Framework
        inArray = (window.HighchartsAdapter && window.HighchartsAdapter.inArray) || HC.inArray,

    // cache prototypes
        axisProto = HC.Axis.prototype,
        tickProto = HC.Tick.prototype,

    // cache original methods
        _axisInit = axisProto.init,
        _axisRender = axisProto.render,
        _axisSetCategories = axisProto.setCategories,
        _tickGetLabelSize = tickProto.getLabelSize,
        _tickAddLabel = tickProto.addLabel,
        _tickDestroy = tickProto.destroy,
        _tickRender = tickProto.render;

    function Category(obj, parent) {
        this.userOptions = deepClone(obj);
        this.name = obj.name || obj;
        this.parent = parent;

        return this;
    }

    Category.prototype.toString = function () {
        var parts = [],
            cat = this;

        while (cat) {
            parts.push(cat.name);
            cat = cat.parent;
        }

        return parts.join(', ');
    };


// Highcharts methods
    function defined(obj) {
        return obj !== UNDEFINED && obj !== null;
    }

// returns sum of an array
    function sum(arr) {
        var l = arr.length,
            x = 0;

        while (l--)
            x += arr[l];

        return x;
    }


// Builds reverse category tree
    function buildTree(cats, out, options, parent, depth) {
        var len = cats.length,
            cat;

        depth || (depth = 0);
        options.depth || (options.depth = 0);

        while (len--) {
            cat = cats[len];


            if (parent)
                cat.parent = parent;


            if (cat.categories)
                buildTree(cat.categories, out, options, cat, depth + 1);

            else
                addLeaf(out, cat, parent);
        }

        options.depth = mathMax(options.depth, depth);
    }

// Adds category leaf to array
    function addLeaf(out, cat, parent) {
        out.unshift(new Category(cat, parent));

        while (parent) {
            parent.leaves++ || (parent.leaves = 1);
            parent = parent.parent;
        }
    }

// Pushes part of grid to path
    function addGridPart(path, d, width) {
        // Based on crispLine from HC (#65)
        if (d[0] === d[2]) {
            d[0] = d[2] = mathRound(d[0]) - (width % 2 / 2);
        }
        if (d[1] === d[3]) {
            d[1] = d[3] = mathRound(d[1]) + (width % 2 / 2);
        }

        path.push(
            'M',
            d[0], d[1],
            'L',
            d[2], d[3]
        );
    }

// Destroys category groups
    function cleanCategory(category) {
        var tmp;

        while (category) {
            tmp = category.parent;

            if (category.label)
                category.label.destroy();

            delete category.parent;
            delete category.label;

            category = tmp;
        }
    }

// Returns tick position
    function tickPosition(tick, pos) {
        return tick.getPosition(tick.axis.horiz, pos, tick.axis.tickmarkOffset);
    }

    function walk(arr, key, fn) {
        var l = arr.length,
            children;

        while (l--) {
            children = arr[l][key];

            if (children)
                walk(children, key, fn);

            fn(arr[l]);
        }
    }

    function deepClone(thing) {
        return JSON.parse(JSON.stringify(thing));
    }

//
// Axis prototype
//
    axisProto.init = function (chart, options) {
        // default behaviour
        _axisInit.call(this, chart, options);
        this.groupedCategoriesInit = false;

    };


    axisProto.formatOuter = function (span, time) {
        var date = new Date(time);
        return _dateUtils.FORMAT_MONTH_DAY_YEAR.format(date);
    };

    axisProto.createDateBuckets = function () {
        var categories = [];
        if (this.type == "datetime" || !isDefined(this.tickPositions) || this.tickPositions.length == 0) return categories;
        var span;
        var begin;
        var end;
        var usedDates = [];
        if (this.tickPositions.length > 1) {
            begin = _dateUtils.getMidnight(this.tickPositions[0]);
            end = _dateUtils.getMidnight(this.tickPositions[this.tickPositions.length - 1]);
        } else {
            begin = end = _dateUtils.getMidnight(this.tickPositions[0]);
        }
        span = end - begin;

        usedDates.push(begin);
        if (isNaN(span)) return categories; //no ticks yet

        var lastTime = begin;

        var that = this;
        var category = {name: that.formatOuter(span, begin), categories: []};
        categories.push(category);

        this.tickPositions.forEach(function (time) {
            var tick = this.ticks[time];
            //no ticks yet
            if ( !isDefined( tick ) ) return;
            var tickLabel = tick.label.textStr;
            category.categories.push(tickLabel);
            if (time - lastTime > _dateUtils.MILLIS_PER_DAY) {
                //it's a brand new day
                lastTime = _dateUtils.getMidnight(time);
                category = new Category({name: that.formatOuter(span, lastTime)});
                category.categories = [];
                categories.push(category);
            }
        }.bind(this));

        return categories;
    }

    /**
     * Create a categories compatible for ticks (indexed by the time, instead of array index that is used now)
     * @param categories
     */
    axisProto.rearrangeTickCategories = function (categories) {
        if (!isDefined(categories) || categories.length == 0) return categories;
        var reindexedCategories = {};
        categories.forEach(function (category) {
            var positions = this.tickPositions;
            var ticks = this.ticks;
            for ( var index = 0; index < positions.length; index++ ) {
                var tickPosition = positions[ index ];
                var tickLabel = ticks[ tickPosition ].label.textStr;
                if ( tickLabel == category.name ) {
                    reindexedCategories[tickPosition] = category;
                }
            }
        }.bind(this));
        return reindexedCategories;
    }

    axisProto.addGroupLabels = function () {
        if (!isDefined( this.categories ) || !isDefined( this.tickPositions ) || this.tickPositions.length == 0 ) return;
        this.tickPositions.forEach( function( tickPosition ) {
            var tick = this.ticks[ tickPosition ];
            var category = this.categories[tickPosition ];
            if ( !isValued( category.parent.tick ) ) {
                tick.addGroupedLabels( category );
            }
        }.bind( this ) );

    }
// setup required axis options
    axisProto.setupGroups = function (options) {
        var categories,
            reverseTree = [],
            stats       = {};

        categories = deepClone(options.categories);;

        // build categories tree
        buildTree(categories, reverseTree, stats);

        // set axis properties
        this.categoriesTree   = categories;
        this.categories       = this.rearrangeTickCategories(reverseTree);
        this.isGrouped        = stats.depth !== 0;
        this.labelsDepth      = stats.depth;
        this.labelsSizes      = [];
        this.labelsGridPath   = [];
        this.tickLength       = options.tickLength || this.tickLength || null;
        // #66: tickWidth for x axis defaults to 1, for y to 0
        this.tickWidth        = pick(options.tickWidth, this.isXAxis ? 1 : 0);
        this.directionFactor  = [-1, 1, 1, -1][this.side];

        this.options.lineWidth = pick(options.lineWidth, 1);
    };



    axisProto.render = function () {
        // clear grid path
        if (this.isGrouped) {
            this.labelsGridPath = [];
        }
        // cache original tick length
        if (this.originalTickLength === UNDEFINED)
            this.originalTickLength = this.options.tickLength;

        // use default tickLength for not-grouped axis
        // and generate grid on grouped axes,
        // use tiny number to force highcharts to hide tick
        //this.options.tickLength = this.isGrouped ? 0.001 : this.originalTickLength;

        _axisRender.call(this);

        if (this.isXAxis) {
            this.setCategories( this.createDateBuckets(), false );
        }

        if (!this.isGrouped ) {
            if (this.labelsGrid)
                this.labelsGrid.attr({visibility: 'hidden'});
            return;
        }

        var axis = this,
            options = axis.options,
            top = axis.top,
            left = axis.left,
            right = left + axis.width,
            bottom = top + axis.height,
            visible = axis.hasVisibleSeries || (typeof axis.hasData === "function" ? axis.hasData() : axis.hasData),
            depth = axis.labelsDepth,
            grid = axis.labelsGrid,
            horiz = axis.horiz,
            d = axis.labelsGridPath,
            i = options.drawHorizontalBorders === false ? depth + 1 : 0,
            offset = axis.opposite ? (horiz ? top : right) : (horiz ? bottom : left),
            tickWidth = axis.tickWidth,
            part;

        if (axis.userTickLength)
            depth -= 1;

        // render grid path for the first time
        if (!grid) {
            grid = axis.labelsGrid = axis.chart.renderer.path()
                .attr({
                    // #58: use tickWidth/tickColor instead of lineWidth/lineColor:
                    strokeWidth: tickWidth, // < 4.0.3
                    'stroke-width': tickWidth, // 4.0.3+ #30
                    stroke: options.tickColor
                })
                .add(axis.axisGroup);
        }

        // go through every level and draw horizontal grid line
        while (i <= depth) {
            offset += axis.groupSize(i);

            part = horiz ?
                [left, offset, right, offset] :
                [offset, top, offset, bottom];

            addGridPart(d, part, tickWidth);
            i++;
        }

        // draw grid path
        grid.attr({
            d: d,
            visibility: visible ? 'visible' : 'hidden'
        });


        if ( isValued( axis.labelGroup ) ) {
            axis.labelGroup.attr({
                visibility: visible ? 'visible' : 'hidden'
            });
        }


        walk(axis.categoriesTree, 'categories', function (group) {
            var tick = group.tick;

            if (!tick)
                return;

            if (tick.startAt + tick.leaves - 1 < axis.min || tick.startAt > axis.max) {
                tick.label.hide();
                tick.destroyed = 0;
            }
            else
                tick.label.attr({
                    visibility: visible ? 'visible' : 'hidden'
                });
        });
    };

    axisProto.setCategories = function (newCategories, doRedraw) {
        if (this.categories)
            this.cleanGroups();
        this.setupGroups({
            categories: newCategories
        });
        this.addGroupLabels();
//        _axisSetCategories.call(this, this.categories, doRedraw);
    };

// cleans old categories
    axisProto.cleanGroups = function () {
        var ticks = this.ticks,
            n;

        for (n in ticks)
            if (ticks[n].parent)
                delete ticks[n].parent;

        walk(this.categoriesTree, 'categories', function (group) {
            var tick = group.tick,
                n;

            if (!tick)
                return;

            tick.label.destroy();

            for (n in tick)
                delete tick[n];

            delete group.tick;
        });
        this.labelsGrid = null;
    };

// keeps size of each categories level
    axisProto.groupSize = function (level, position) {
        var positions = this.labelsSizes,
            direction = this.directionFactor,
            groupedOptions = this.options.labels.groupedOptions ? this.options.labels.groupedOptions[level - 1] : false,
            userXY = 0;

        if (groupedOptions) {
            if (direction == -1) {
                userXY = groupedOptions.x ? groupedOptions.x : 0;
            } else {
                userXY = groupedOptions.y ? groupedOptions.y : 0;
            }
        }

        if (position !== UNDEFINED)
            positions[level] = mathMax(positions[level] || 0, position + 10 + Math.abs(userXY));

        if (level === true)
            return sum(positions) * direction;

        else if (positions[level])
            return positions[level] * direction;

        return 0;
    };


//
// Tick prototype
//

// Override methods prototypes
    tickProto.addLabel = function () {
        var category;
        var categoryCache = this.axis.categories;
        var xAxis = this.axis.isXAxis;
        if ( xAxis ) {
            //we have existing categories but this is a new point/tick
            if ( typeof this.axis.categories != "undefined" && typeof this.axis.categories[ this.pos ] == "undefined" ) {
                categoryCache = this.axis.categories;
                delete this.axis.categories;
            }
        }

        _tickAddLabel.call(this);

        if ( xAxis && isValued( categoryCache ) ) this.axis.categories = categoryCache;

        if (!this.axis.categories || !(category = this.axis.categories[this.pos]))
            return;

        // set label text - but applied after formatter #46
        if (this.label)
            this.label.attr('text', this.axis.labelFormatter.call({
                axis: this.axis,
                chart: this.axis.chart,
                isFirst: this.isFirst,
                isLast: this.isLast,
                value: category.name
            }));

        // create elements for parent categories
    };

// render ancestor label
    tickProto.addGroupedLabels = function (category) {
        var tick = this,
            axis = this.axis,
            chart = axis.chart,
            options = axis.options.labels,
            useHTML = options.useHTML,
            css = options.style,
            userAttr = options.groupedOptions,
            attr = {align: 'center', rotation: options.rotation, x: 0, y: 0},
            size = axis.horiz ? 'height' : 'width',
            depth = 0,
            label;


        while (tick) {
            if (depth > 0 && !category.tick) {
                // render label element
                this.value = category.name;
                var name = options.formatter ? options.formatter.call(this, category) : category.name,
                    hasOptions = userAttr && userAttr[depth - 1],
                    mergedAttrs = hasOptions ? merge(attr, userAttr[depth - 1]) : attr,
                    mergedCSS = hasOptions && userAttr[depth - 1].style ? merge(css, userAttr[depth - 1].style) : css;

                //#63: style is passed in CSS and not as an attribute
                delete mergedAttrs.style;

                label = chart.renderer.text(name, 0, 0, useHTML)
                    .attr(mergedAttrs)
                    .css(mergedCSS)
                    .add(axis.labelGroup);

                // tick properties
                tick.startAt = this.pos;
                tick.childCount = category.categories.length;
                tick.leaves = category.leaves;
                tick.visible = this.childCount;
                tick.label = label;
                tick.labelOffsets = {
                    x: mergedAttrs.x,
                    y: mergedAttrs.y
                };

                // link tick with category
                category.tick = tick;
            }

            // set level size
            axis.groupSize(depth, tick.label.getBBox()[size]);

            // go up to the parent category
            category = category.parent;

            if (category)
                tick = tick.parent = category.tick || {};
            else
                tick = null;

            depth++;
        }
    };

// set labels position & render categories grid
    tickProto.render = function (index, old, opacity) {
        _tickRender.call(this, index, old, opacity);
        var axis = this.axis;

        if ( typeof axis =="undefined" || !axis.categories ) return;

        var treeCat = axis.categories[this.pos];

        if ( !axis.isGrouped || !treeCat || this.pos > axis.max)
            return;

        var tick = this,
            group = tick,
            axis = tick.axis,
            tickPos = tick.pos,
            isFirst = tick.isFirst,
            max = axis.max,
            min = axis.min,
            horiz = axis.horiz,
            cat = axis.categories[tickPos],
            grid = axis.labelsGridPath,
            size = axis.groupSize(0),
            tickLen = axis.tickLength || size,
            tickWidth = axis.tickWidth,
            factor = axis.directionFactor,
            xy = tickPosition(tick, tickPos),
            start = horiz ? xy.y : xy.x,
            baseLine = axis.chart.renderer.fontMetrics(axis.options.labels.style.fontSize).b,
            depth = 1,
        // adjust grid lines for edges
            reverseCrisp = ((horiz && xy.x === axis.pos + axis.len) || (!horiz && xy.y === axis.pos)) ? -1 : 0,
            gridAttrs,
            lvlSize,
            minPos,
            maxPos,
            attrs,
            bBox;

        // render grid for "normal" categories (first-level), render left grid line only for the first category
        if (isFirst) {

            gridAttrs = horiz ?
                [axis.left, xy.y, axis.left, xy.y + axis.groupSize(true)] :
                axis.isXAxis ?
                    [xy.x, axis.top, xy.x + axis.groupSize(true), axis.top] :
                    [xy.x, axis.top + axis.len, xy.x + axis.groupSize(true), axis.top + axis.len];

            addGridPart(grid, gridAttrs, tickWidth);
        }


        if (horiz && axis.left < xy.x) {
            addGridPart(grid, [xy.x - reverseCrisp, xy.y, xy.x - reverseCrisp, xy.y + size], tickWidth);
        } else if (!horiz && axis.top <= xy.y) {
            addGridPart(grid, [xy.x, xy.y + reverseCrisp, xy.x + size, xy.y + reverseCrisp], tickWidth);
        }

        size = start + size;

        function fixOffset(group, treeCat, tick) {
            var ret = 0;
            if (isFirst) {
                ret = inArray(treeCat.name, treeCat.parent.categories);
                ret = ret < 0 ? 0 : ret;
                return ret;
            }
            return ret;
        }


        while (group = group.parent) {
            var fix = fixOffset(group, treeCat, tick),
                userX = group.labelOffsets.x,
                userY = group.labelOffsets.y;

            minPos = tickPosition(tick, mathMax(group.startAt - 1, min - 1));
            maxPos = tickPosition(tick, mathMin(group.startAt + group.leaves - 1 - fix, max));
            bBox = group.label.getBBox(true);
            lvlSize = axis.groupSize(depth);
            // check if on the edge to adjust
            reverseCrisp = ((horiz && maxPos.x === axis.pos + axis.len) || (!horiz && maxPos.y === axis.pos)) ? -1 : 0;

            attrs = horiz ? {
                x: (minPos.x + maxPos.x) / 2 + userX,
                y: size + lvlSize / 2 + baseLine - bBox.height / 2 - 4 + userY / 2
            } : {
                x: size + lvlSize / 2 + userX,
                y: (minPos.y + maxPos.y - bBox.height) / 2 + baseLine + userY
            };

            if (!isNaN(attrs.x) && !isNaN(attrs.y)) {
                group.label.attr(attrs);

                if (grid) {
                    if (horiz && axis.left < maxPos.x) {
                        addGridPart(grid, [maxPos.x - reverseCrisp, size, maxPos.x - reverseCrisp, size + lvlSize], tickWidth);
                    } else if (!horiz && axis.top <= maxPos.y) {
                        addGridPart(grid, [size, maxPos.y + reverseCrisp, size + lvlSize, maxPos.y + reverseCrisp], tickWidth);
                    }
                }
            }

            size += lvlSize;
            depth++;
        }
    };

    tickProto.destroy = function () {
        var group = this;

        while (group = group.parent)
            group.destroyed++ || (group.destroyed = 1);

        _tickDestroy.call(this);
    };

// return size of the label (height for horizontal, width for vertical axes)
    tickProto.getLabelSize = function () {
        if (this.axis.isGrouped === true) {
            // #72, getBBox might need recalculating when chart is tall
            var size = _tickGetLabelSize.call(this) + 10,
                topLabelSize = this.axis.labelsSizes[0];
            if (topLabelSize < size) {
                this.axis.labelsSizes[0] = size;
            }

            return sum(this.axis.labelsSizes);
        } else
            return _tickGetLabelSize.call(this);
    };

}(Highcharts));
